import fs from 'fs';
import test from 'ava';

import * as ohm from '../../index.mjs';
import {semanticsForToAST, toAST} from '../../extras/semantics-toAST.js';

const g = ohm.grammar(fs.readFileSync('test/data/arithmetic.ohm'));

// --------------------------------------------------------------------
// Tests
// --------------------------------------------------------------------

test('semantic action', t => {
  const semantics = semanticsForToAST(g);
  const matchResult = g.match('10 + 20');

  t.truthy(
      'toAST' in semantics._getSemantics().operations,
      'toAST operation added to semantics',
  );
  t.truthy(semantics(matchResult).toAST, 'toAST operation added to match result');
});

test('default', t => {
  let matchResult = g.match('10 + 20');
  let ast = toAST(matchResult);
  let expected = {
    0: '10',
    2: '20',
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper default AST');

  const g2 = ohm.grammar('G { Mix = a? b* "|" a? b* a = "a" b = "b" }');
  matchResult = g2.match('a|bb');
  ast = toAST(matchResult, {});
  expected = {
    0: 'a',
    1: [],
    3: null,
    4: ['b', 'b'],
    type: 'Mix',
  };
  t.deepEqual(ast, expected, 'proper optionals and lists in AST');
});

test('mapping', t => {
  let matchResult = g.match('10 + 20');
  let ast = toAST(matchResult, {
    AddExp_plus: {
      expr1: 0,
      expr2: 2,
    },
  });
  let expected = {
    expr1: '10',
    expr2: '20',
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper AST with mapped properties');

  ast = toAST(matchResult, {
    AddExp_plus: {
      expr1: 0,
      op: 1,
      expr2: 2,
    },
  });
  expected = {
    expr1: '10',
    op: '+',
    expr2: '20',
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper AST with explicitly mapped property');

  ast = toAST(matchResult, {
    AddExp_plus: {
      0: 0,
    },
  });
  expected = {
    0: '10',
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper AST with explicitly removed property');

  ast = toAST(matchResult, {
    AddExp_plus: {
      0: 0,
      type: undefined,
    },
  });
  expected = {
    0: '10',
  };
  t.deepEqual(ast, expected, 'proper AST with explicitly removed type');

  ast = toAST(matchResult, {
    AddExp_plus: {
      expr1: 0,
      op: 'plus',
      expr2: 2,
    },
  });
  expected = {
    expr1: '10',
    op: 'plus',
    expr2: '20',
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper AST with static property');

  ast = toAST(matchResult, {
    AddExp_plus: {
      expr1: Object(0),
      op: 'plus',
      expr2: Object(2),
    },
  });
  expected = {
    expr1: 0,
    op: 'plus',
    expr2: 2,
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper AST with boxed number property');

  ast = toAST(matchResult, {
    AddExp_plus: {
      expr1: 0,
      expr2: 2,
      str(children) {
        return children
            .map(function(child) {
              return child.toAST(this.args.mapping);
            }, this)
            .join('');
      },
    },
  });
  expected = {
    expr1: '10',
    expr2: '20',
    str: '10+20',
    type: 'AddExp_plus',
  };
  t.deepEqual(ast, expected, 'proper AST with computed property');

  matchResult = g.match('10 + 20 - 30');
  ast = toAST(matchResult, {
    AddExp_plus: 2,
  });
  expected = {
    0: '20', // child 2 of AddExp_plus
    2: '30',
    type: 'AddExp_minus',
  };
  t.deepEqual(ast, expected, 'proper AST with forwarded child node');

  ast = toAST(matchResult, {
    AddExp_plus(expr1, _, expr2) {
      expr1 = expr1.toAST(this.args.mapping);
      expr2 = expr2.toAST(this.args.mapping);
      return 'plus(' + expr1 + ', ' + expr2 + ')';
    },
  });
  expected = {
    0: 'plus(10, 20)', // child 2 of AddExp_plus
    2: '30',
    type: 'AddExp_minus',
  };
  t.deepEqual(ast, expected, 'proper AST with computed node/operation extension');

  ast = toAST(matchResult, {
    Exp: {
      type: 'Exp',
      0: 0,
    },
  });
  expected = {
    0: {
      0: {
        0: '10',
        2: '20',
        type: 'AddExp_plus',
      },
      2: '30',
      type: 'AddExp_minus',
    },
    type: 'Exp',
  };
  t.deepEqual(ast, expected, 'proper AST with explicity reintroduced node');
});

test('real examples (combinations)', t => {
  const matchResult = g.match('10 + 20 - 30');

  let ast = toAST(matchResult, {
    AddExp_plus: {
      expr1: 0,
      op: 1,
      expr2: 2,
      type: 'Expression',
    },
    AddExp_minus: {
      expr1: 0,
      op: 1,
      expr2: 2,
      type: 'Expression',
    },
  });
  let expected = {
    expr1: {
      expr1: '10',
      expr2: '20',
      op: '+',
      type: 'Expression',
    },
    expr2: '30',
    op: '-',
    type: 'Expression',
  };
  t.deepEqual(ast, expected, 'proper AST for arithmetic example #1');

  ast = toAST(matchResult, {
    AddExp_plus: {
      augend: 0,
      addend: 2,
      type: 'AddExpression',
    },
    AddExp_minus: {
      minuend: 0,
      subtrahend: 2,
      type: 'SubExpression',
    },
  });
  expected = {
    minuend: {
      augend: '10',
      addend: '20',
      type: 'AddExpression',
    },
    subtrahend: '30',
    type: 'SubExpression',
  };
  t.deepEqual(ast, expected, 'proper AST for arithmetic example #2');
});

test('usage errors', t => {
  t.throws(() => toAST(g.match('doesnotmatch')), {
    message: /toAST\(\) expects a succesful MatchResult as first parameter/,
  });
  t.throws(() => toAST({}), {
    message: /toAST\(\) expects a succesful MatchResult as first parameter/,
  });
  t.throws(() => semanticsForToAST({}), {
    message: /semanticsToAST\(\) expects a Grammar as parameter/,
  });
});

test('listOf and friends - #394', t => {
  // By default, toAST assumes that lexical rules represent indivisible tokens,
  // but that doesn't make sense for listOf, nonemptyListOf, and emptyListOf.
  const g = ohm.grammar(`
    G {
      Exp = listOf<digit, "+">
      Exp2 = ListOf<digit, "+">
    }
  `);

  const ast = (input, mapping) => toAST(g.match(input), mapping);
  const astSyntactic = input => toAST(g.match(input, 'Exp2'));

  // By default, the `listOf` action should pass through, and both `nonemptyListOf`
  // and `emptyListOf` should return an array.
  t.deepEqual(ast('3+5'), ['3', '5']);
  t.deepEqual(ast(''), []);

  // The AST should be the same whether we use `listOf` or `ListOf`.
  t.deepEqual(ast('3+5'), astSyntactic('3 + 5'));
  t.deepEqual(ast(''), astSyntactic(''));

  // Ensure that it's still be possible to override the default mappings.

  t.is(
      ast('0+1', {
        nonemptyListOf: (first, sep, rest) => 'XX',
      }),
      'XX',
  );

  t.is(
      ast('1+2', {
        nonemptyListOf: 0,
      }),
      '1',
  );

  t.is(
      ast('', {
        emptyListOf: () => 'nix',
      }),
      'nix',
  );
});
